(function (){

    function parseCSVTokens(buffer){
        var result = { tokens:[], index:0 };
        var regexp = /(,|\n|\r|[^,\"\n\r]+|\"(\"\"|[^\"]+)*\")/g;
        var r = null;
        var p = 0;
        while (null != (r = regexp.exec(buffer)) && p == r.index){
            result.tokens.push(r[0]);
            p += r[0].length;
        }
        result.index = p;
        result.reminder = (p==buffer.length)?"":buffer.slice(p - buffer.length);
        return result;
    }

    function checkColumns(c,r,res){
        if(c > 0 && c != r.length ){ 
            res.errors.push( 
            {
                message:"unexpected number of columns: "+r.length+", expected: "+c+"!"
            });
        }
    }

    function openCSV(){
        return { 
            data : [], errors : [], warnings : [], 
            prev: undefined, error:undefined, warning:undefined,
            cols : 0, pos : 0, record:[]
        };
    }

    function parseCSVBuffer(csv, buffer){
        if(!csv.input){ 
            csv.input = parseCSVTokens ( buffer );
        }else{
            csv.input = parseCSVTokens ( 
                ""+csv.input.tokens.join() + ((csv.input.reminder)?csv.input.reminder:"") + buffer );
        }

        if ( !buffer || buffer.length == 0 ) { 
            csv.warning = {message:"empty buffer!"}; 
        }
        
        while ( csv.input.tokens.length > 1 ) {
            var token = csv.input.tokens.shift();
            processCSVToken(csv,token);
        }
    }

    function processCSVToken(csv,token){
            switch (token.charAt(0)) {
                case "," : ++csv.pos; 
                    if (!csv.prev || /^(,|\r|\n)/.test(csv.prev)) { csv.record.push(""); }
                    break; 
                case "\"" : csv.pos += token.length; 
                    if (!csv.prev || /^(,|\r|\n)/.test(csv.prev)) {
                        csv.record.push(token.slice(1,-1).replace(/\"\"/g,"\""));
                    } else { 
                        csv.error = {message:"unexpected character '\n'!"};  
                    } 
                    break; 
                case "\n" : ++csv.pos; 
                    if (!csv.prev || /^,/.test(csv.prev)) { csv.record.push(""); }
                    checkColumns(csv.cols,csv.record,csv);
                    if(0==csv.cols){csv.cols=csv.record.length};
                    csv.data.push(csv.record);
                    if(csv.error){csv.errors.push(csv.error);csv.error=undefined;}
                    if(csv.warning){csv.warnings.push(csv.warning);csv.warning=undefined;}
                    csv.record = []; 
                    break; 
                case "\r" : ++csv.pos; break; 
                default : csv.pos += token.length; 
                    if (!csv.prev || /^(,|\r|\n)/.test(csv.prev)) {
                        csv.record.push(token);
                    } else { 
                        csv.error = {message:"unexpected character '"+token[0]+"'!"};
                    } 
                    break;
            }
            csv.prev = token;
    }

    function closeCSV(csv){
        while ( csv.input.tokens.length > 0 ) {
            var token = csv.input.tokens.shift();
            processCSVToken(csv,token);
        }
        if(csv.record.length){
            checkColumns(csv.cols,csv.record,csv);
            csv.data.push(csv.record);
            if(csv.error){csv.errors.push(csv.error);csv.error=undefined;}
            if(csv.warning){csv.warnings.push(csv.warning);csv.warning=undefined;}
        }
        delete csv.input; delete csv.pos; delete csv.cols;
        delete csv.prev; delete csv.record; delete csv.warning;
        delete csv.error;
        return csv;
    }

    function parseAll ( buffer ){
        var csv = openCSV();
        var max = 1024*20;
        for(var i=0 ; i < buffer.length; i+=max){
            parseCSVBuffer(csv, buffer.slice(i,(i+max < buffer.length)?i+max:buffer.length));
        }
        return closeCSV(csv);
    }

    function parseCSV ( buffer, callback ){
        if(callback){
            setTimeout(function (){
                var csv = parseAll(buffer);
                callback(csv.data, csv.errors, csv.warnings);
            });
        }else{
            return parseAll(buffer);
        }
    }

    nCSV = function (onComplete){ 
        if ( !(this instanceof nCSV) ) { return new nCSV(); }
        this.onCompleteCallback = onComplete;
        this.csv = openCSV();
    }

    nCSV.prototype.write = function(buffer){
        var max = 1024*90;
        for(var i=0 ; i < buffer.length; i+=max){
            parseCSVBuffer(this.csv, buffer.slice(i,(i+max < buffer.length)?i+max:buffer.length));
        }
    }

    nCSV.prototype.close = function(){
        this.onCompleteCallback(closeCSV(this.csv).data);
    }

    nCSV.parse = parseCSV;
    nCSV.open = openCSV;
    nCSV.write = parseCSVBuffer;
    nCSV.close = closeCSV;
    nCSV.tokenize = parseCSVTokens;

    if('object' == typeof exports){
	module.exports = nCSV;
    }

})();

